/*******************************************************************************
#                                                                              #
#      MJPG-streamer 允许多个输出插件从一个输入插件获取JPEG帧                  #
#                                                                              #
#      Copyright (C) 2007 Tom Stöveken                                         #
#                                                                              #
# 本程序是自由软件；您可根据自由软件基金会发布的 GNU 通用公共许可证第二版      #
# 的条款重新分发和/或修改它。                                                  #
#                                                                              #
# 本代码基于原始的 output_file.c 修改。                                        #
# 由 Dimitrios Zachariadis 及后续开发者为实现命令驱动的流和快照功能而增强。    #
# 版本: 1.1 (多输出环境优化版)                                                 #
#                                                                              #
# 这个输出插件作为一个UDP命令服务器。当输入插件使用 pthread_cond_broadcast     #
# 广播新帧时，它可以与其他输出插件（如 output_http）协同工作。                 #
#                                                                              #
*******************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <signal.h>
#include <sys/socket.h>
#include <resolv.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <getopt.h>
#include <pthread.h>
#include <fcntl.h>
#include <time.h>
#include <syslog.h>
#include <dirent.h>

#include "../../utils.h"
#include "../../mjpg_streamer.h"

#define OUTPUT_PLUGIN_NAME "UDP命令输出插件"
#define MAX_FILENAME_LEN 256
#define MAX_COMMAND_LEN 64

// 插件的状态机
typedef enum {
    IDLE,       // 空闲状态，等待指令
    STREAMING   // 推流状态
} plugin_state;

static pthread_t worker;
static globals *pglobal;
static int delay_ms, max_frame_size;
static char *folder = "/tmp";
static unsigned char *frame_buffer = NULL;
static char *command_on_save = NULL;
static int input_number = 0;
static int udp_port = 0;

// 状态管理变量
static plugin_state current_state = IDLE;
static struct sockaddr_in streaming_client_addr;

/******************************************************************************
 * 打印此插件的帮助信息
 ******************************************************************************/
void help(void)
{
    fprintf(stderr, " ----------------------------------------------------------------------------\n" \
            " 帮助信息..: "OUTPUT_PLUGIN_NAME"\n" \
            " ----------------------------------------------------------------------------\n" \
            " 本插件通过监听UDP命令来推流视频或保存快照。\n" \
            " 可用参数如下:\n\n" \
            " [-p | --port ]..........: 必须。监听命令的UDP端口。\n" \
            " [-i | --input ].........: 从指定输入插件获取帧 (默认: 0)。\n" \
            " [-f | --folder ]........: 保存快照的目录 (默认: /tmp)。\n" \
            " [-d | --delay ].........: 每次动作后的延迟(毫秒) (默认: 0)。\n" \
            " [-c | --command ].......: 保存图片后执行的Shell命令。\n\n" \
            " UDP 命令 (发送至指定端口):\n" \
            "   'get_stream'          : 开始向发送方推流视频。\n" \
            "   'stop_stream'         : 停止视频流。\n" \
            "   'get_photo <文件名>'  : 在服务器上保存单帧图像到 <目录>/<文件名>。\n" \
            " ----------------------------------------------------------------------------\n");
}

/******************************************************************************
 * 清理已分配的资源。此函数会被注册到线程清理栈中，以确保线程退出时被调用。
 ******************************************************************************/
void worker_cleanup(void *arg)
{
    OPRINT("清理UDP工作线程分配的资源...\n");
    if(frame_buffer != NULL) {
        free(frame_buffer);
        frame_buffer = NULL;
    }
}

/******************************************************************************
 * 将给定的帧数据保存到配置目录下的文件中。
 ******************************************************************************/
int save_frame_to_file(const char *filename, const unsigned char *buffer, int size) {
    char filepath[PATH_MAX];
    snprintf(filepath, sizeof(filepath), "%s/%s", folder, filename);

    int fd = open(filepath, O_CREAT | O_RDWR | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);
    if(fd < 0) {
        LOG("错误：无法打开文件 %s: %s\n", filepath, strerror(errno));
        return -1;
    }

    ssize_t written_bytes = write(fd, buffer, size);
    if(written_bytes < 0 || written_bytes != size) {
        LOG("错误：无法向文件 %s 写入 %d 字节: %s\n", size, filepath, strerror(errno));
        close(fd);
        return -1;
    }

    close(fd);
    OPRINT("快照已保存: %s\n", filepath);

    // 如果指定了保存后命令，则执行它
    if (command_on_save != NULL) {
        char full_command[1024];
        snprintf(full_command, sizeof(full_command), "%s \"%s\"", command_on_save, filepath);
        DBG("正在执行命令: %s\n", full_command);
        if (system(full_command) != 0) {
            LOG("警告：保存后执行命令失败。\n");
        }
    }
    return 0;
}

/******************************************************************************
 * 主工作线程。它建立一个UDP服务器，然后进入一个循环来处理命令和推流视频。
 ******************************************************************************/
void *worker_thread(void *arg)
{
    int frame_size = 0, sd;
    unsigned char *tmp_realloc_buffer = NULL;

    pthread_cleanup_push(worker_cleanup, NULL);

    // 设置UDP套接字
    struct sockaddr_in server_addr;
    if ((sd = socket(PF_INET, SOCK_DGRAM, 0)) < 0) {
        LOG("致命错误：无法创建套接字: %s\n", strerror(errno));
        pthread_exit(NULL);
    }

    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(udp_port);
    server_addr.sin_addr.s_addr = INADDR_ANY;

    if(bind(sd, (struct sockaddr*)&server_addr, sizeof(server_addr)) != 0) {
        LOG("致命错误：无法绑定到端口 %d: %s\n", udp_port, strerror(errno));
        close(sd);
        pthread_exit(NULL);
    }
    
    // 设置为非阻塞，防止recvfrom阻塞视频流
    if(fcntl(sd, F_SETFL, fcntl(sd, F_GETFL, 0) | O_NONBLOCK) < 0) {
        LOG("致命错误：fcntl设置非阻塞失败: %s\n", strerror(errno));
        close(sd);
        pthread_exit(NULL);
    }

    OPRINT("UDP命令服务器正在监听端口 %d\n", udp_port);

    while (!pglobal->stop) {
        struct sockaddr_in client_addr;
        socklen_t addr_len = sizeof(client_addr);
        char udp_buffer[1024] = {0};
        
        // 步骤 1: 检查新命令 (非阻塞)
        ssize_t bytes = recvfrom(sd, udp_buffer, sizeof(udp_buffer) - 1, 0, (struct sockaddr*)&client_addr, &addr_len);
        if (bytes > 0) {
            udp_buffer[bytes] = '\0';
            char cmd[MAX_COMMAND_LEN] = {0};
            char param[MAX_FILENAME_LEN] = {0};
            sscanf(udp_buffer, "%63s %255s", cmd, param);
            DBG("收到UDP命令: '%s', 参数: '%s' 来自 %s:%d\n", cmd, param, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));

            if (strcmp(cmd, "get_stream") == 0) {
                current_state = STREAMING;
                memcpy(&streaming_client_addr, &client_addr, sizeof(streaming_client_addr));
                OPRINT("UDP状态 -> STREAMING (推流中)，客户端: %s:%d\n", inet_ntoa(streaming_client_addr.sin_addr), ntohs(streaming_client_addr.sin_port));
                sendto(sd, "OK: STREAM_STARTED", 18, 0, (struct sockaddr*)&client_addr, addr_len);
            } 
            else if (strcmp(cmd, "stop_stream") == 0) {
                OPRINT("UDP状态 -> IDLE (空闲)\n");
                current_state = IDLE;
                sendto(sd, "OK: STREAM_STOPPED", 18, 0, (struct sockaddr*)&client_addr, addr_len);
            }
            else if (strcmp(cmd, "get_photo") == 0) {
                if (strlen(param) > 0) {
                    pthread_mutex_lock(&pglobal->in[input_number].db);
                    pthread_cond_wait(&pglobal->in[input_number].db_update, &pglobal->in[input_number].db);
                    
                    frame_size = pglobal->in[input_number].size;
                    if(frame_size > max_frame_size) {
                        max_frame_size = frame_size + (1 << 16); // 增加64KB
                        tmp_realloc_buffer = realloc(frame_buffer, max_frame_size);
                        if(tmp_realloc_buffer == NULL) {
                            pthread_mutex_unlock(&pglobal->in[input_number].db);
                            LOG("错误: 快照帧内存不足。\n");
                            sendto(sd, "错误: 服务器内存不足", 27, 0, (struct sockaddr*)&client_addr, addr_len);
                            continue;
                        }
                        frame_buffer = tmp_realloc_buffer;
                    }
                    memcpy(frame_buffer, pglobal->in[input_number].buf, frame_size);
                    pthread_mutex_unlock(&pglobal->in[input_number].db);
                    
                    if(save_frame_to_file(param, frame_buffer, frame_size) == 0){
                        char ok_msg[512];
                        snprintf(ok_msg, sizeof(ok_msg), "OK: 照片已保存到 %s/%s", folder, param);
                        sendto(sd, ok_msg, strlen(ok_msg), 0, (struct sockaddr*)&client_addr, addr_len);
                    } else {
                        sendto(sd, "错误: 服务器保存照片失败", 32, 0, (struct sockaddr*)&client_addr, addr_len);
                    }
                } else {
                    sendto(sd, "错误: get_photo 命令需要文件名", 36, 0, (struct sockaddr*)&client_addr, addr_len);
                }
            }
        }

        // 步骤 2: 如果是 STREAMING 状态, 获取帧并发送
        if (current_state == STREAMING) {
            pthread_mutex_lock(&pglobal->in[input_number].db);
            // 等待来自输入插件的广播信号
            pthread_cond_wait(&pglobal->in[input_number].db_update, &pglobal->in[input_number].db);

            frame_size = pglobal->in[input_number].size;
            if(frame_size > max_frame_size) {
                max_frame_size = frame_size + (1 << 16);
                tmp_realloc_buffer = realloc(frame_buffer, max_frame_size);
                if(tmp_realloc_buffer == NULL) {
                    pthread_mutex_unlock(&pglobal->in[input_number].db);
                    LOG("错误: 推流内存不足，停止推流。\n");
                    current_state = IDLE;
                    continue;
                }
                frame_buffer = tmp_realloc_buffer;
            }
            memcpy(frame_buffer, pglobal->in[input_number].buf, frame_size);
            pthread_mutex_unlock(&pglobal->in[input_number].db);

            sendto(sd, frame_buffer, frame_size, 0, (struct sockaddr*)&streaming_client_addr, sizeof(streaming_client_addr));
        } else {
             // 如果是 IDLE 状态, 休眠以防100% CPU占用
            usleep(10000); // 休眠10毫秒
        }

        if (delay_ms > 0) {
            usleep(1000 * delay_ms);
        }
    }

    OPRINT("收到关闭信号，正在关闭UDP套接字。\n");
    close(sd);
    pthread_cleanup_pop(1); // 执行清理函数
    return NULL;
}

/*** 插件接口函数 ***/

int output_init(output_parameter *param)
{
    delay_ms = 0;
    param->argv[0] = OUTPUT_PLUGIN_NAME;

    reset_getopt();
    while(1) {
        int option_index = 0, c;
        static struct option long_options[] = {
            {"h", no_argument, 0, 'h'}, {"help", no_argument, 0, 'h'},
            {"p", required_argument, 0, 'p'}, {"port", required_argument, 0, 'p'},
            {"i", required_argument, 0, 'i'}, {"input", required_argument, 0, 'i'},
            {"f", required_argument, 0, 'f'}, {"folder", required_argument, 0, 'f'},
            {"d", required_argument, 0, 'd'}, {"delay", required_argument, 0, 'd'},
            {"c", required_argument, 0, 'c'}, {"command", required_argument, 0, 'c'},
            {0, 0, 0, 0}
        };

        c = getopt_long_only(param->argc, param->argv, "hp:i:f:d:c:", long_options, &option_index);
        if(c == -1) break;

        switch(c) {
            case 'h': help(); return 1;
            case 'p': udp_port = atoi(optarg); break;
            case 'i': input_number = atoi(optarg); break;
            case 'f':
                if (folder != NULL && strcmp(folder, "/tmp") != 0) free(folder);
                folder = strdup(optarg);
                break;
            case 'd': delay_ms = atoi(optarg); break;
            case 'c':
                if (command_on_save != NULL) free(command_on_save);
                command_on_save = strdup(optarg);
                break;
            default: help(); return 1;
        }
    }

    if (udp_port <= 0 || udp_port > 65535) {
        LOG("致命错误: 必须通过 -p 或 --port 提供一个有效的UDP端口 (1-65535)。\n");
        return 1;
    }

    pglobal = param->global;
    if(!(input_number < pglobal->incnt)) {
        LOG("致命错误: 输入插件 #%d 不可用, 总共只加载了 %d 个插件。\n", input_number, pglobal->incnt);
        return 1;
    }
    
    OPRINT("插件.........: %s\n", OUTPUT_PLUGIN_NAME);
    OPRINT(" - 端口........: %d\n", udp_port);
    OPRINT(" - 输入源......: #%d (%s)\n", input_number, pglobal->in[input_number].plugin);
    OPRINT(" - 目录........: %s\n", folder);
    OPRINT(" - 延迟........: %d ms\n", delay_ms);
    OPRINT(" - 命令........: %s\n", (command_on_save == NULL) ? "禁用" : command_on_save);
    
    return 0;
}

int output_stop(int id)
{
    OPRINT("正在停止 %s...\n", OUTPUT_PLUGIN_NAME);
    //
    // 这是更安全的方式来停止线程：通过全局标志位通知线程自行退出，
    // 而不是强制取消(pthread_cancel)，后者可能导致资源泄漏。
    // 主程序会等待线程 `pthread_join` 完成。
    //
    return 0;
}

int output_run(int id)
{
    // pglobal->stop 在主程序中控制，这里无需设置
    if (pthread_create(&worker, 0, worker_thread, NULL) != 0) {
        LOG("错误：创建UDP输出工作线程失败\n");
        return 1;
    }
    // 让线程在后台运行，主程序不直接等待它
    pthread_detach(worker);
    return 0;
}